[AWS CDK] CloudFront Functions でリクエスト URL に index.html を追加する構成を Next.js アプリケーションで試してみた
こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。
前回のエントリで、CloudFront + S3 で静的サイトホスティングをしている Web アプリケーションへのリクエスト URL に index.html
を自動追加する構成を作成しました。
そして上記実装が活用できる Web アプリケーションフレームワークとして Next.js があります。
今回は、Next.js アプリケーションでのリクエスト URL に index.html
を CloudFront Functions で追加する構成を AWS CDK で構築してみました。
試してみた
モノリポ環境作成の準備
AWS CDK と Next.js を同じリポジトリで管理するために、モノリポ環境を npm workspaces 利用します。ここではその準備を行います。
プロジェクトフォルダを作成します。
mkdir cdk-nextjs-sample && cd $_
Git の初期化を行います。
git init touch .gitignore echo "node_modules/" > .gitignore
npm の初期化を行います。
npm init -y
Next.js アプリケーションの初期化
次に Next.js アプリケーションの初期化を、モノリポ環境の packages/web
ディレクトリで行います。
ワークスペースを新規作成します。
npm init -w packages/web -y rm packages/web/package.json
Next.js アプリケーションの初期化を行います。
$ (cd packages/web && npx create-next-app@latest --typescript) ✔ What is your project named? … . ✔ Would you like to use ESLint? … No / Yes ✔ Would you like to use Tailwind CSS? … No / Yes ✔ Would you like to use `src/` directory? … No / Yes ✔ Would you like to use App Router? (recommended) … No / Yes ✔ Would you like to customize the default import alias (@/*)? … No / Yes
next.config.js
を設定します。output
を export
に設定することで、next build
の生成物を out
ディレクトリに出力するようにし、AWS CDK でのコンテンツアップロードのソースパスに指定できるようにします。また、trailingSlash
を true
に設定し、各ページのファイル名が一律で index.html
になるようにします。例えば既定では about.html
と出力されるページは、この設定により /about/inde.html
に出力されるようになります。
/** @type {import('next').NextConfig} */ const nextConfig = { output: "export", // `next build` の生成物を `out` ディレクトリに出力する trailingSlash: true, // ルート以外のページの出力パスを `out/<Path>/index.html` とする }; module.exports = nextConfig;
Next.js アプリケーションのページ作成
ルートおよび /about
パスのページを作成します。
/about
ディレクトリを作成します。
mkdir packages/web/src/app/about
/about/page.tsx
ファイルを次の内容で作成します。
import Link from 'next/link' export default function Page() { return ( <> <h1>About Us</h1> <Link href="/">Back to HOME</Link> </> ) }
またルートにある page.tsx
を次のように修正します。
import Link from 'next/link' export default function Page() { return ( <> <h1>Home</h1> <Link href="/about">Go to AboutUs</Link> </> ) }
これにより両ページ間の遷移を Link
タグにより実装できました。
ここでローカルで Web アプリケーションを起動してみます。
npm run dev -w web
http://localhost:3000 にアクセスし、リンクによる遷移が正常に動作することを確認します。
AWS CDK アプリケーションの初期化
次に CDK アプリケーションの初期化を、モノリポ環境の packages/iac
ディレクトリで行います。
npm init -w packages/iac -y rm packages/iac/package.json
CDK Init を実行します。
(cd packages/iac && cdk init sample-app --language typescript)
CDK アプリケーションの依存関係を最新バージョンにアップデートします。
npm i aws-cdk-lib@latest aws-cdk@latest -w iac
AWS CDK コードの実装
AWS CDK のスタックを定義するコードを実装します。
import { aws_cloudfront, aws_s3, aws_s3_deployment, aws_cloudfront_origins, Stack, RemovalPolicy, Duration, CfnOutput, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class IacStack extends Stack { constructor(scope: Construct, id: string) { super(scope, id); // S3 バケットの作成 const websiteBucket = new aws_s3.Bucket(this, 'WebsiteBucket', { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); // CloudFront から S3 バケットへのアクセスを許可するために、 // Origin Access Identity を作成し、S3 バケットのアクセスポリシーに追加する const originAccessIdentity = new aws_cloudfront.OriginAccessIdentity( this, 'OriginAccessIdentity' ); websiteBucket.grantRead(originAccessIdentity); // CloudFront Function の作成 const cloudFrontFunction = new aws_cloudfront.Function( this, 'AddSecurityHeadersToTheResponseFunction', { code: aws_cloudfront.FunctionCode.fromFile({ filePath: 'src/cloudfront-function/add-index-html-to-request-url/index.js', }), // JavaScript runtime 2.0 を指定 runtime: aws_cloudfront.FunctionRuntime.JS_2_0, } ); // CloudFront Destribution を作成 const distribution = new aws_cloudfront.Distribution(this, 'Distribution', { errorResponses: [ { ttl: Duration.minutes(5), httpStatus: 404, responseHttpStatus: 404, responsePagePath: '/404.html', }, ], defaultBehavior: { viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, origin: new aws_cloudfront_origins.S3Origin(websiteBucket, { originAccessIdentity, }), // CloudFront Function と Distribution の関連付け functionAssociations: [ { function: cloudFrontFunction, eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST, }, ], }, }); // CloudFront Distribution のドメイン名を出力 new CfnOutput(this, 'DistributionUrl', { value: `https://${distribution.distributionDomainName}`, }); // S3 バケットへのコンテンツのデプロイ、CloudFront Distribution のキャッシュ削除 new aws_s3_deployment.BucketDeployment(this, 'WebsiteDeploy', { distribution, destinationBucket: websiteBucket, distributionPaths: ['/*'], sources: [aws_s3_deployment.Source.asset("./../web/out")], // Next.js のビルド生成物の出力パスを指定 }); } }
CloudFront Functions コードの実装
リクエスト URL に index.html
を追加する CloudFront Functions のコードを実装します。
コードを配置するディレクトリを作成します。
mkdir packages/iac/src mkdir packages/iac/src/cloudfront-function mkdir packages/iac/src/cloudfront-function/add-index-html-to-request-url
コードの実装は次のようになります。公式ドキュメントのサンプルコードをそのまま利用しています。
async function handler(event) { const request = event.request; const uri = request.uri; // Check whether the URI is missing a file name. if (uri.endsWith('/')) { request.uri += 'index.html'; } // Check whether the URI is missing a file extension. else if (!uri.includes('.')) { request.uri += '/index.html'; } return request; }
既定では .js
ファイルが .gitignore
の対象になっているため、上記のコードを除外するようにします。
echo "!src/cloudfront-function/**/*.js" >> packages/iac/.gitignore
デプロイ
Next.js アプリケーションをビルドします。
npm run build -w web
これにより out
ディレクトリ配下に、index.html
および about/index.html
が出力されます。
CDK デプロイを実行します。
npm run cdk -w iac -- deploy --require-approval never --method=direct
動作確認
デプロイされた Web アプリケーションにアクセスして動作を確認します。
CloudFront Distribution のドメイン名 https://xxxxxxxx.cloudfront.net にアクセスすると、ルートページが正常に表示されます。リクエスト URL への /index.html
の追加処理が行われていることが確認できます。
ルートページから <Link>
を利用した /about
パスのページへの遷移も正常に動作します。
https://xxxxxxxx.cloudfront.net/about または https://xxxxxxxx.cloudfront.net/about/ に直接アクセスした場合でも、正常にページが表示されます。この場合も /index.html
の追加処理が正常に行われていることが確認できました。
その他
index.html の自動追加を行わない場合
CloudFront Functions の関連付け設定を外して、index.html
の自動追加を行わない場合の動作を試してみます。
// CloudFront Destribution を作成 const distribution = new aws_cloudfront.Distribution(this, 'Distribution', { errorResponses: [ { ttl: Duration.minutes(5), httpStatus: 404, responseHttpStatus: 404, responsePagePath: '/404.html', }, ], defaultBehavior: { viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, origin: new aws_cloudfront_origins.S3Origin(websiteBucket, { originAccessIdentity, }), // // CloudFront Function と Distribution の関連付け // functionAssociations: [ // { // function: cloudFrontFunction, // eventType: aws_cloudfront.FunctionEventType.VIEWER_REQUEST, // }, // ], }, });
この場合は /
および /about
のいずれへのアクセスもパスが存在せずエラーとなります。
正常にアクセスするためにはリクエスト URL でファイル名 index.html
まで指定する必要があります。
trailingSlash オプションを設定しない場合
next.config.js
で trailingSlash
オプションを設定せず、デフォルトのままにした場合の動作を試してみます。
/** @type {import('next').NextConfig} */ const nextConfig = { output: "export", // `next build` の生成物を `out` ディレクトリに出力する //trailingSlash: true, // ルート以外のページの出力パスを `out/<Path>/index.html` とする }; module.exports = nextConfig;
npm run build -w web
(next build
)コマンドを実行してアプリケーションをビルドすると、out
ディレクトリへの出力は次のようになります。/about/index.html
ではなく /about.html
となっていることが確認できます。
この場合は今回は CloudFront Functions のコードでは対応できなくなります。ルート以外へのリクエスト URL に .html
を付与する処理となるようにコードを修正する必要があります。
おわりに
Next.js アプリケーションでのリクエスト URL に index.html
を CloudFront Functions で追加する構成を AWS CDK で構築してみました。
別解として HttpOrigin を利用して S3 ウェブサイトエンドポイントをオリジンにし、無理やり index.html
を付与させる方法もありますが、その場合は OAI(または OAC)による認証が使用できないなどの不都合があるので、Next.js アプリケーションを CloudFront + S3 でホスティングする場合は、今回紹介した方法が良いかと思います。
参考
以上